package moe.util;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import moe.dao.pool.ConnectionManager;

public class PreparedQueryParser {
	public static interface ConditionParser {
		public QueryBuilder parse(String[] querys, QueryBuilder builder,
				PreparedParameters parameters);
	}

	public static class PreparedParameters {
		private ArrayList<Integer> sqlTypes = new ArrayList<Integer>();
		private ArrayList<Object> parameters = new ArrayList<Object>();

		public Object[] getParameters() {
			return parameters.toArray();
		}

		public <T> void add(T param, int sqlType) {
			parameters.add(param);
			sqlTypes.add(sqlType);
		}

		public void bind(PreparedStatement stat) {
			try {
				for (int i = 0, length = parameters.size(); i < length; i++) {
					stat.setObject(i + 1, parameters.get(i), sqlTypes.get(i));
				}
			} catch (SQLException e) {
				throw new RuntimeException(e);
			}
		}
	}

	public class NumberConditionParser implements ConditionParser {
		private String name;

		public NumberConditionParser(String name) {
			this.name = name;
		}

		public QueryBuilder parse(String[] querys, QueryBuilder builder,
				PreparedParameters parameters) {
			for (String query : querys) {
				try {
					String[] arr = query.split(" ");
					if (arr.length != 2) {
						continue;
					}
					String operate = arr[0];
					long number = Long.valueOf(arr[1]);
					builder.and();
					parametric(builder, parameters, operate, name, number, Types.INTEGER);
				} catch (Exception e) {
					continue;
				}
			}
			return builder;
		}
	}

	private static Map<String, Method[]> queryBuilderMethodMap;
	private static Map<String, ConditionParser> parserMap;

	private static PreparedQueryParser instance;
	private static final Object lock = new Object();
	private static final String ESCAPE_CHAR = ConnectionManager.getDefaultManager()
			.getSearchStringEscape();

	public static PreparedQueryParser getInstance() {
		synchronized (lock) {
			if (instance == null) {
				instance = new PreparedQueryParser();
			}
		}
		return instance;
	}

	private PreparedQueryParser() {
		parserMap = new HashMap<String, ConditionParser>();
		initInsideParser();
	}

	private void escape(QueryBuilder builder, PreparedParameters parameters) {
		builder.escape(QueryBuilder.PLACEHOLDER);
		parameters.add(ESCAPE_CHAR, Types.VARCHAR);
	}

	private <T> void parametric(QueryBuilder builder, PreparedParameters parameters,
			String operate, String field, T value, int type) throws IllegalArgumentException,
			IllegalAccessException, InvocationTargetException {
		invokeOperateMethod(builder, operate, field, QueryBuilder.PLACEHOLDER);
		parameters.add(value, type);
	}

	private static void invokeOperateMethod(Object o, String operate, Object... args)
			throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
		if (null == queryBuilderMethodMap) {
			queryBuilderMethodMap = new HashMap<String, Method[]>();
			for (Method m : QueryBuilder.class.getDeclaredMethods()) {
				if (queryBuilderMethodMap.containsKey(m.getName())) {
					Method[] old = queryBuilderMethodMap.get(m.getName());
					Method[] now = new Method[old.length + 1];
					System.arraycopy(old, 0, now, 0, old.length);
					now[old.length] = m;
					queryBuilderMethodMap.put(m.getName(), now);
				} else {
					queryBuilderMethodMap.put(m.getName(), new Method[] { m });
				}
			}
		}
		Method[] methods = queryBuilderMethodMap.get(operate);
		if (methods == null) {
			return;
		}
		Method method = null;
		_NEXT: for (Method m : methods) {
			Class[] classes = m.getParameterTypes();
			if (args.length != classes.length) {
				continue;
			}
			for (int i = 0, length = args.length; i < length; i++) {
				if (!classes[i].isInstance(args[i])) {
					continue _NEXT;
				}
			}
			method = m;
			break;
		}
		if (method == null) {
			return;
		}
		method.setAccessible(true);
		method.invoke(o, args);
	}

	private void initInsideParser() {
		/*
		 * id between long [eq, neq, gt, ge, lt, le] id=ge 7415[&id=eq 845645]
		 */
		registerParser("id", new NumberConditionParser("id"));
		/*
		 * md5 single string md5=str...
		 */
		registerParser("md5", new ConditionParser() {
			public QueryBuilder parse(String[] querys, QueryBuilder builder,
					PreparedParameters parameters) {
				String md5 = querys[0];
				builder.and().eq("md5", QueryBuilder.PLACEHOLDER);
				parameters.add(md5, Types.VARCHAR);
				return builder;
			}
		});
		/*
		 * rating enum {e q s} [eq, neq] rating=eq e
		 */
		registerParser("rating", new ConditionParser() {
			public QueryBuilder parse(String[] querys, QueryBuilder builder,
					PreparedParameters parameters) {
				String[] arr = querys[0].split(" ");
				builder.and();
				try {
					parametric(builder, parameters, arr[0], "rating", arr[1], Types.VARCHAR);
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
				return builder;
			}
		});
		/*
		 * score between int [eq, neq, gt, ge, lt, le] score=lt 15[&score=eq 78]
		 */
		registerParser("score", new NumberConditionParser("score"));
		/*
		 * has_children boolean has_children=true
		 */
		registerParser("has_children", new ConditionParser() {
			public QueryBuilder parse(String[] querys, QueryBuilder builder,
					PreparedParameters parameters) {
				boolean has_children = Boolean.valueOf(querys[0]);
				builder.and().eq("has_children", QueryBuilder.PLACEHOLDER);
				parameters.add(has_children, Types.BOOLEAN);
				return builder;
			}
		});
		/*
		 * parent_id single long parent_id=75453
		 */
		registerParser("parent_id", new ConditionParser() {
			public QueryBuilder parse(String[] querys, QueryBuilder builder,
					PreparedParameters parameters) {
				long parent_id = Long.valueOf(querys[0]);
				builder.and().eq("parent_id", QueryBuilder.PLACEHOLDER);
				parameters.add(parent_id, Types.INTEGER);
				return builder;
			}
		});
		/*
		 * tags include, exclude [eq, neq, like, notlike] tags=eq loli lolita[&tags=notlike tony game]
		 */
		registerParser("tags", new ConditionParser() {
			public QueryBuilder parse(String[] querys, QueryBuilder builder,
					PreparedParameters parameters) {
				for (String query : querys) {
					String[] arr = query.split(" ");
					String mark = arr[0];
					String operate = "like";
					if (mark.startsWith("n")) {
						operate = "notlike";
					}

					for (int i = 1; i < arr.length; i++) {
						arr[i] = arr[i]
								.replace(ESCAPE_CHAR, ESCAPE_CHAR + ESCAPE_CHAR)
								.replace("%", ESCAPE_CHAR + "%")
								.replace("_", ESCAPE_CHAR + "_");
						if ("eq".equals(mark) || "neq".equals(mark)) {
							try {
								builder.and();
								builder.sub();
								parametric(builder, parameters, operate, "tags", arr[i] + " %",
										Types.VARCHAR);
								escape(builder, parameters);
								builder.or();
								parametric(builder, parameters, operate, "tags", "% " + arr[i],
										Types.VARCHAR);
								escape(builder, parameters);
								builder.or();
								parametric(builder, parameters, operate, "tags",
										"% " + arr[i] + " %", Types.VARCHAR);
								escape(builder, parameters);
								builder.end();
							} catch (Exception e) {
								continue;
							}
						} else {
							try {
								builder.and();
								parametric(builder, parameters, operate, "tags",
										"%" + arr[i] + "%", Types.VARCHAR);
								escape(builder, parameters);
							} catch (Exception e) {
								continue;
							}
						}
					}
				}
				return builder;
			}
		});
		/*
		 * width between int [eq, neq, gt, ge, lt, le] width=gt 233[&width=lt 54865]
		 */
		registerParser("width", new NumberConditionParser("width"));
		/*
		 * height between int [eq, neq, gt, ge, lt, le] height=gt 233[&height=lt 54865]
		 */
		registerParser("height", new NumberConditionParser("height"));
		/*
		 * file_size between long [eq, neq, gt, ge, lt, le] file_size=gt 233[&file_size=lt 54865]
		 */
		registerParser("file_size", new ConditionParser() {
			public QueryBuilder parse(String[] querys, QueryBuilder builder,
					PreparedParameters parameters) {
				for (String query : querys) {
					try {
						String[] arr = query.split(" ");
						if (arr.length != 2) {
							continue;
						}
						String operate = arr[0];

						Matcher matcher = Pattern.compile("^([0-9.]+)([km][b]?)?$",
								Pattern.CASE_INSENSITIVE).matcher(arr[1]);
						if (!matcher.matches()) {
							continue;
						}
						float value = Float.valueOf(matcher.group(1));
						String unit = matcher.group(2);
						if ("k".equalsIgnoreCase(unit) || "kb".equalsIgnoreCase(unit)) {
							value *= 1024;
						} else if ("m".equalsIgnoreCase(unit) || "mb".equalsIgnoreCase(unit)) {
							value *= 1024 * 1024;
						}

						builder.and();
						parametric(builder, parameters, operate, "file_size", (long) value,
								Types.INTEGER);
					} catch (Exception e) {
						continue;
					}
				}
				return builder;
			}
		});
		/*
		 * aspect_ratio between string e.i 4/3, ... [eq, neq, gt, ge, lt, le] aspect_ratio=eq 16/9[&aspect_ratio=gt 4/3]
		 */
		registerParser("aspect_ratio", new ConditionParser() {
			public QueryBuilder parse(String[] querys, QueryBuilder builder,
					PreparedParameters parameters) {
				for (String query : querys) {
					try {
						String[] arr = query.split(" ");
						if (arr.length != 2) {
							continue;
						}
						String operate = arr[0];
						arr = arr[1].split("/");
						final int w = Integer.valueOf(arr[0]);
						int h = Integer.valueOf(arr[1]);
						String propertyName = "`width` * " + h;
						Object value = new Object() {
							@Override
							public String toString() {
								return "`height` * " + w;
							}
						};

						builder.and();
						invokeOperateMethod(builder, operate, propertyName, value);
					} catch (Exception e) {
						continue;
					}
				}
				return builder;
			}
		});
		/*
		 * order single string [asc desc] [id score width height file_size aspect_ratio]
		 */
		registerParser("order", new ConditionParser() {
			public QueryBuilder parse(String[] querys, QueryBuilder builder,
					PreparedParameters parameters) {
				String[] arr = querys[0].split(" ");
				String operate = arr[0];
				String value = arr[1];
				if ("aspect_ratio".equals(value)) {
					value = "width`/`height";
				}
				builder.order(value, "asc".equals(operate));
				return builder;
			}
		});
		/*
		 * sql SQL WHERE expression
		 */
		registerParser("sql", new ConditionParser() {
			public QueryBuilder parse(String[] querys, QueryBuilder builder,
					PreparedParameters parameters) {
				return builder.and().where(querys[0]);
			}
		});
	}

	public void registerParser(String name, ConditionParser parser) {
		parserMap.put(name, parser);
	}

	public boolean hasRegister(String name) {
		return parserMap.containsKey(name);
	}

	public ConditionParser getParser(String name) {
		return parserMap.get(name);
	}

	public PreparedStatement build(Map<String, String[]> query, QueryBuilder builder,
			Connection conn) {
		PreparedParameters parameters = parse(query, builder);
		return bind(builder.build(), parameters, conn);
	}

	public PreparedParameters parse(Map<String, String[]> query, QueryBuilder builder) {
		PreparedParameters parameters = new PreparedParameters();
		for (String key : query.keySet()) {
			try {
				getParser(key).parse(query.get(key), builder, parameters);
			} catch (Exception e) {
				continue;
			}
		}
		return parameters;
	}

	public PreparedStatement bind(String sql, PreparedParameters parameters, Connection conn) {
		PreparedStatement preparedStatement = null;
		try {
			preparedStatement = conn.prepareStatement(sql);
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
		parameters.bind(preparedStatement);
		return preparedStatement;
	}
}
